package com.forcetools.forcediff;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;

import org.apache.log4j.Logger;

import com.forcetools.utils.MetadataMarshaller;
import com.forcetools.utils.PackageHelper;
import com.forcetools.utils.ConnectionUtils;
import com.forcetools.utils.PropertyConfigUtils;
import com.sforce.soap.metadata.AsyncRequestState;
import com.sforce.soap.metadata.AsyncResult;
import com.sforce.soap.metadata.CustomObject;
import com.sforce.soap.metadata.FileProperties;
import com.sforce.soap.metadata.Metadata;
import com.sforce.soap.metadata.MetadataConnection;
import com.sforce.soap.metadata.Package;
import com.sforce.soap.metadata.RetrieveMessage;
import com.sforce.soap.metadata.RetrieveRequest;
import com.sforce.soap.metadata.RetrieveResult;
public class OrgMetadata {
	private MetadataConnection metaConn;
	private String orgCfgName;
	private String pkgCfgName;
	private Package pkgCfg;
	private static Logger logger;
	static{
		logger = Logger.getLogger(OrgMetadata.class);
	}
	public OrgMetadata(String orgCfgName,String pkgCfgName)throws Exception{
		this.orgCfgName=orgCfgName;
		this.pkgCfgName=pkgCfgName;
		initialize();
	}
	
	private void initialize() throws Exception{
		pkgCfg = PackageHelper.getPackageConfig(pkgCfgName);
		this.metaConn = ConnectionUtils.getMetadataConnection(orgCfgName);
	}
	
	public void retreiveMetadata() throws Exception{
		RetrieveRequest retReq = new RetrieveRequest();
		retReq.setApiVersion(Double.valueOf(pkgCfg.getVersion()));
		retReq.setUnpackaged(pkgCfg);
		Integer maxPolls = Integer.valueOf(PropertyConfigUtils.getProperty("sfdc_connection",this.orgCfgName,"metadataPollMax"));
		Integer pollWaitSec = Integer.valueOf(PropertyConfigUtils.getProperty("sfdc_connection",this.orgCfgName,"metadataPollingInterval"));
		long waitTimeMillis = pollWaitSec*1000;
		Integer currPollCnt=0;
        try{
            AsyncResult asyncResult = metaConn.retrieve(retReq);
            System.out.println("Async Request ID:"+asyncResult.getId());
	        while (!asyncResult.isDone()) {
	        	
	        	Thread.sleep(waitTimeMillis);
		        // double the wait time for the next iteration  
	        	waitTimeMillis *= 2;
		        if (currPollCnt++ > maxPolls) {
			    	logger.error("Maximum poll limit exceeded. Unable to fetch metadata. Reduce the components in your package configuration and try again");
		        	throw new Exception("Maximum poll limit exceeded. Unable to fetch metadata. Reduce the components in your package configuration and try again");
			    }
	        	try{
			        asyncResult = metaConn.checkStatus(
			        		new String[] {asyncResult.getId()})[0];
	        	}catch(Exception ex){
			    	logger.error(ex);
		        	throw ex;
	        	}
		    }
		    if (asyncResult.getState() != AsyncRequestState.Completed) {
		    	logger.error(asyncResult.getStatusCode() + " msg: " +asyncResult.getMessage());
		        throw new Exception(asyncResult.getStatusCode() + " msg: " +asyncResult.getMessage());
		    }
		    RetrieveResult result = metaConn.checkRetrieveStatus(asyncResult.getId());
		    logMessagesAndWarnings(result);
		    extractContents(result);
		    // Print out any warning messages
        }catch(Exception ex){
        	
        }
		    
	}

	private void logMessagesAndWarnings(RetrieveResult result){
	    StringBuilder buf = new StringBuilder();
	    if (result.getMessages() != null) {
	        for (RetrieveMessage rm : result.getMessages()) {
	        	buf.append(rm.getFileName() + " - " + rm.getProblem());
	        }
	        buf.append('\n');
	        for(FileProperties fileProp:result.getFileProperties()){
	        	buf.append("Name:"+fileProp.getFileName());
	        	buf.append('\n');
	        	buf.append("Full Name:"+fileProp.getFullName());
	        	buf.append('\n');
	        }
	    }
	    if (buf.length() > 0) {
	    	logger.info("Retrieve warnings:\n" + buf);
	    }	
		
	}
	
	private void extractContents(RetrieveResult result) throws Exception{
	    String parseMode = PropertyConfigUtils.getProperty("package_config",this.orgCfgName,"metaDataParsingMode");
	    if(parseMode==null){
	    	parseMode="MEMORY";
	    }
	    Boolean inMemory = (parseMode.equalsIgnoreCase("MEMORY")?true:false);
    	ZipInputStream zipIn=null;
	    try{
		    if(!inMemory){
		    	zipIn = createTempZipFile(result);
		    }else{
		    	zipIn = initializeInMemoryZip(result);
		    }
		    parseMetaData(zipIn);
	    }finally{
	    	zipIn.close();
	    }
	}
	
	private void parseMetaData(ZipInputStream zipIn) throws Exception{
		ZipEntry currEntry= zipIn.getNextEntry();
		Metadata currMetaData=null;
		MetadataMarshaller marshaller = new MetadataMarshaller();
		while(currEntry!=null){
			if(!currEntry.isDirectory() && currEntry.getName().indexOf("package.xml")==-1){
				currMetaData = marshaller.parseFromXml(extractXMLFromZip(zipIn));
				zipIn.closeEntry();
				logger.debug("Entry:"+currEntry.getName()+",Object Parsed:"+((CustomObject)currMetaData).getPluralLabel());
			}
			currEntry = zipIn.getNextEntry();
		}
		zipIn.close();
		
	}
	
	private ByteArrayInputStream extractXMLFromZip(ZipInputStream zipIn) throws Exception{
		int count;
		ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); 
        byte data[] = new byte[2048];
        while ((count = zipIn.read(data, 0, 2048)) != -1) {
        	byteOut.write(data, 0, count);
        }		
		return new ByteArrayInputStream(byteOut.toByteArray());
	}
    private void copy(ReadableByteChannel src, WritableByteChannel dest)throws IOException
    {
        // use an in-memory byte buffer  
    
        ByteBuffer buffer = ByteBuffer.allocate(8092);
        while (src.read(buffer) != -1) {buffer.flip();
            while(buffer.hasRemaining()) {dest.write(buffer);
            }buffer.clear();
        }
    }	
    

	private ZipInputStream createTempZipFile(RetrieveResult result) throws Exception{
	    ByteArrayInputStream bais = new ByteArrayInputStream(result.getZipFile());
	    String tempDir = PropertyConfigUtils.getProperty("package_config",this.orgCfgName,"tempFolder");
		File temp =  new File(tempDir);
	    if(!temp.exists()){
	    	temp.mkdir();
	    }
	    SimpleDateFormat dtFormat = new SimpleDateFormat("MM-dd-yyyy-hhmmss");
	    String fileName = "METADATA-"+this.orgCfgName+"-"+dtFormat.format(new Date())+".zip";
	    File resultsFile = new File(tempDir,fileName);
	    FileOutputStream os = new FileOutputStream(resultsFile);
        try {ReadableByteChannel src = Channels.newChannel(bais);
            FileChannel dest = os.getChannel();
            copy(src, dest);
            logger.debug("Results written to " + resultsFile.getAbsolutePath());
        }
        finally {
        	os.close();
        }	
        return new ZipInputStream(new BufferedInputStream(new FileInputStream(resultsFile)));
	}

	private ZipInputStream initializeInMemoryZip(RetrieveResult result) throws Exception{
	    ByteArrayInputStream bais = new ByteArrayInputStream(result.getZipFile());
	    return new ZipInputStream(new BufferedInputStream(bais));
	}

}
